/*
 * @Date: 2023-07-04 13:46:31
 * @LastEditors: Lee 497232807@qq.com
 * @LastEditTime: 2023-08-07 09:29:37
 * @FilePath: \cocos_framework_base\assets\webGame\Common\VersionManager.ts
 */
// 更新状态码
// 0 :更新成功
// 1:不用更新
// 2:获取版本配置文件失败
// 3:获取MD5配置文件失败
// 4:下载单个文件失败
// 5:移动文件失败
// 6:读取包内配置失败
// 7:不支持热更新的版本号
// 8:不支持热更新的渠道
// 9:强制更新

import { assetManager, resources, sys } from "cc"
import { JsonAsset } from "cc"
import { native } from "cc"
import { EDITOR, NATIVE } from "cc/env"
import { js } from "cc"
import { game } from "cc"
import { TextAsset } from "cc"
import { Asset } from "cc"

/**
 * 主包更新状态码
 */
export enum StateCode {
    UPDATE_SUCCESS = 0,             //更新成功
    NO_NEED_UPDATE,                 //不用更新
    GET_VERSION_CONFIG_FAIL,         //获取版本配置文件失败
    GET_MD5_CONFIG_FAIL,            //获取MD5配置文件失败
    DOWNLOAD_FILE_FAIL,             //下载单个文件失败
    MOVE_FILE_FAIL,                 //移动文件失败
    READ_CONFIG_FAIL,               //读取包内配置失败
    UNSUPPORT_HOT_UPDATE,           //不支持热更新的app版本号
    UNSUPPORT_HOT_UPDATE_CHANNEL,   //不支持热更新的渠道
    FORCE_UPDATE,                   //app强制商店更新
}

// 子游戏更新状态码
export enum SubGameState {
    UN_KNOW = 0,        //未知
    NO_NEED_UPDATE,     //不需要更新
    NEED_UPDATE,        //需要更新
    NEED_DOWNLOAD,      //需要下载
    DOWNLOAD_FAIL,      //下载失败
    UPDATE_SUCCESS,     //更新成功
    GET_MD5_CONFIG_FAIL,//获取MD5配置文件失败

}
/**
 * 文件信息
 */
type fileInfo = {
    fileName?: string,
    md5: string,
    size: number,
    targetPath?: string //移动目标目录
}
/**
 * 热更新配置
 */
type hotConfig = {
    scriptVersion: number, //版本号
    files: Map<string, fileInfo> //文件名称:fileInfo
}

/**
 * 单个子游戏信息
 */
type subGame = {
    ID: number, //游戏ID
    version: number //游戏版本号
    files?: hotConfig //文件名称:fileInfo
}
/**
 * 子游戏信息
 */
type subGameInfo = {
    baseUrl: string, //基础地址
    subgames: Map<string, subGame>, //游戏列表
}
/**
 * 远程配置
 */
type remoteConfig = {
    scriptVersion: number, //版本号
    baseUrl: string, //基础地址
    configFile: string, //配置文件
    channels: string[], //支持渠道
    supportBinarys: string[], //支持的版本号
    forcedBinaryVersions: string[], //强制更新的版本号

}

/**
 * 进度回调
 */
type ProgressCall = (downSize: number, totalSize: number) => void
/**
 * 主包更新状态 回调
 */
type FinishCall = (code: StateCode) => void
/**
 * 子游戏更新状态 回调
 */
type SubGameFinishCall = (code: SubGameState, ...args) => void
/**
 * 配置名称
 */
const fileName = "version"
/**
 * 包内配置路径    resources目录下面的路径
 */
const LocalConfigPath: string = fileName

/**
 * 包外配置路径
 */
export let ConfigPath: string = NATIVE ? native.fileUtils.getWritablePath() + "config" : ""
/**
 * 包外配置路径
 */
export let PackageOutPath: string = NATIVE ? native.fileUtils.getWritablePath() + `config/${fileName}` : ""
/**
 * 下载临时目录
 */
export let DownTempDir: string = NATIVE ? native.fileUtils.getWritablePath() + "packageTemp" : "packageTemp"
/**
 * 下载完移动到热更新目录
 */
export let WriteableDir: string = NATIVE ? native.fileUtils.getWritablePath() + "package" : "package"

/**
 * 子游戏下载更新目录名称
 */
export let SubGameDirName: string = "remote"



/**
 * 主包热更新管理类
 */
export class VersionManager {
    /**
     * 热更新地址
     */
    protected hotUrl: string = null

    /**
     * 热更新回调
     */
    protected finishCall: Function = null

    /**
     * 进度回调
     */
    protected progressCall: ProgressCall = null

    /**
     * 本地配置文件
     */
    protected localConfig: hotConfig = null

    /**
    * 远程配置文件
    */
    protected remoteConfig: remoteConfig = null

    /**
     * 远程md5配置文件
     */
    protected remoteMD5Config: hotConfig = null

    /**
     * 总下载大小
     */
    protected totalDownSize: number = 0
    /**
     * 当前下载大小
     */
    protected curDownSize: number = 0
    /**
     * 开始热更新
     */
    protected startUpdate() {

        this.totalDownSize = 0
        this.curDownSize = 0
        //解析本地配置
        this.parseLocalConfig(() => {
            //解析远程配置
            this.parseRemoteConfig((code: number) => {
                if (code == 1)//成功获取远程配置
                {
                    //对比版本号
                    this.compareVersion()
                }
                else //获取远程配置失败
                {
                    this.finishCall(StateCode.GET_VERSION_CONFIG_FAIL)
                }
            })
        })

    }


    /**
     * 解析本地配置
     */
    protected parseLocalConfig(oncomplete: Function) {

        this.localConfig = null
        console.log("parseLocalConfig")
        if (native.fileUtils.isFileExist(PackageOutPath))//包外配置存在
        {
            this.localConfig = JSON.parse(native.fileUtils.getStringFromFile(PackageOutPath))
            oncomplete()
        }
        else//包内配置
        {
            resources.load(LocalConfigPath, (err: Error, asset: TextAsset) => {
                if (err) {
                    this.finishCall(StateCode.READ_CONFIG_FAIL)
                    return
                }
                // console.log("parseLocalConfig===>",asset.text)
                this.localConfig = JSON.parse(asset.text) as hotConfig
                oncomplete()
            })
        }
    }

    /**
     * 解析远程配置
     */
    protected parseRemoteConfig(onFinish: Function) {
        console.log("parseRemoteConfig", this.hotUrl)
        this.sendHttpRequest(this.hotUrl, (err: boolean, response: string) => {
            if (err) {
                onFinish(0)
            }
            else {
                this.remoteConfig = JSON.parse(response) as remoteConfig
                console.log("subGameInfo", this.remoteConfig)
                //根据远程配置的游戏信息 查看本地子游戏信息
                // var subGameInfo = this.remoteConfig.subGameInfo
                // this.parseLocalSubGameInfo(subGameInfo)
                onFinish(1)
            }


        })
    }
    /**
     * 对比版本号
     */
    protected compareVersion() {
        var localVersion = this.getLocalVersion()
        var remoteVersion = this.getRemoteVersion()
        if (localVersion != remoteVersion)//版本号不一致
        {
            var supportBinarys = this.remoteConfig.supportBinarys //支持热更新app版本号
            var forcedBinaryVersions = this.remoteConfig.forcedBinaryVersions //强制更新版本号
            var supportChannel = this.remoteConfig.channels //支持渠道
            var binaryVersion = "1.0.0" //当前app版本号 TODO
            var channel = "0" //当前渠道 TODO
            if (supportBinarys.indexOf(binaryVersion) == -1) //不支持热更新 app版本号
            {
                this.finishCall(StateCode.UNSUPPORT_HOT_UPDATE)
                return
            }
            if (supportChannel.indexOf(channel) == -1) //不支持热更新渠道
            {
                this.finishCall(StateCode.UNSUPPORT_HOT_UPDATE_CHANNEL)
                return
            }

            if (forcedBinaryVersions.indexOf(binaryVersion) != -1) //app强制商店更新
            {
                this.finishCall(StateCode.FORCE_UPDATE)
                return
            }

            //获取远程md5配置
            this.getRemoteMD5Config((code: number) => {
                if (code == 1) //对比差异文件
                {
                    var remoteFiles = this.remoteMD5Config.files //远程文件列表
                    var localFiles = this.localConfig.files //本地文件列表
                    var changeList = this.compareChangeList(localFiles, remoteFiles)
                    var platform = this.getPlatForm()
                    var baseUrl = `${this.remoteConfig.baseUrl}/${platform}/version_${this.remoteConfig.scriptVersion}/`//js.formatStr(this.remoteConfig.baseUrl, platform,this.remoteConfig.scriptVersion)
                    this.downChangeFiles(baseUrl, changeList, (fileName: string) => {
                        this.finishCall(StateCode.DOWNLOAD_FILE_FAIL, fileName) //下载单个文件失败
                    }, (downSize: number, totalSize: number) => {
                        this.progressCall(downSize, totalSize)     //进度回调
                    }, (moveList: fileInfo[]) => {
                        this.moveFiles(moveList, () => {
                            //移动完成
                            this.updateSuccess()
                        })
                    })
                }
                else {
                    this.finishCall(StateCode.GET_MD5_CONFIG_FAIL) //获取MD5配置文件失败
                }
            })
        }
        else //版本号一致 不需要更新
        {
            this.finishCall(StateCode.NO_NEED_UPDATE)
        }
    }
    /**
     * 对比差异文件
     */
    protected compareChangeList(localFiles: Map<string, fileInfo>, remoteFiles: Map<string, fileInfo>): fileInfo[] {
        console.log("compareChangeList")
        this.curDownSize = 0
        this.totalDownSize = 0


        var changeList: fileInfo[] = [] //差异文件列表
        for (var key in remoteFiles) {
            var remoteFile = remoteFiles[key]
            var localFile = localFiles[key]
            if (!localFile || localFile.md5 != remoteFile.md5) //本地文件不存在 或者md5不一致
            {
                this.totalDownSize = this.totalDownSize + remoteFile.size
                var file: fileInfo = {
                    fileName: key,
                    md5: remoteFile.md5,
                    size: remoteFile.size
                }
                changeList.push(file)
            }
        }
        return changeList


    }

    /**
     * 获取文件目录
     * @param url 
     * @returns 
     */
    protected getDirByUrl(url: string): string {
        var arr = url.split("/")
        var path = ""
        if (arr.length > 1) {
            for (var i = 0; i < arr.length - 1; i++) {
                var tempdir = arr[i]
                if (i == 0) {
                    path = tempdir

                }
                else {
                    path = path + "/" + tempdir
                }
            }
        }
        else {
            path = arr[0]
        }
        path = path + "/"
        return path
    }

    /**
     * 下载差异文件
     * @param baseUrl
     * @param changeList 
     * @param errCall 
     * @param progressCall 
     * @param onComplete 
     */
    protected downChangeFiles(baseUrl: string, changeList: fileInfo[], errCall: (fileName: string) => void, progressCall: ProgressCall, onComplete: Function) {
        console.log("downChangeFiles", changeList.length, baseUrl)
        let curIndex = 0

        let downloader = new native.Downloader()

        let moveList: fileInfo[] = [] //需要移动的文件列表
        let downFile = () => {
            var file: fileInfo = changeList.pop()
            if (file) {
                moveList.push(file)
                var downFileUrl = `${baseUrl}${file.fileName}`
                console.log("downFileUrl", downFileUrl)

                var path = DownTempDir + "/" + file.fileName //临时目录
                var pathDir = this.getDirByUrl(path)
                if (!native.fileUtils.isDirectoryExist(pathDir))//临时目录不存在
                {
                    native.fileUtils.createDirectory(pathDir)
                }
                let task = downloader.createDownloadTask(downFileUrl, path);

            }
            else //下载完成
            {
                onComplete(moveList)
            }
        }

        downloader.onProgress = (task: native.DownloadTask, bytesReceived: number, totalBytesReceived: number, totalBytesExpected: number) => {
            this.curDownSize = this.curDownSize + bytesReceived
            progressCall(this.curDownSize, this.totalDownSize)
            // console.log("onProgress=======>",bytesReceived,totalBytesReceived,totalBytesExpected)
        }

        downloader.onSuccess = (task: native.DownloadTask) => {
            curIndex = curIndex + 1
            downFile()
        }


        downloader.onError = (task: native.DownloadTask, errorCode: number, errorCodeInternal: number, errorStr: string) => {
            errCall(task.requestURL)
        }

        downFile()
    }
    /**
     * 移动下载的文件到热更新目录
     * @param moveList 
     * @param onComplete 
     */
    protected moveFiles(moveList: fileInfo[], onComplete: Function) {
        console.log("moveFiles", moveList.length)
        for (var i = 0; i < moveList.length; i++) {
            var file = moveList[i]
            var path = DownTempDir + "/" + file.fileName //临时目录
            var targetPath = file.targetPath ? file.targetPath : WriteableDir + "/" + file.fileName //热更新目录
            var targetPathDir = this.getDirByUrl(targetPath)
            console.log("moveFiles", path, targetPath)
            if (!native.fileUtils.isDirectoryExist(targetPathDir))//热更新目录不存在
            {
                native.fileUtils.createDirectory(targetPathDir)
            }

            if (file.fileName.endsWith(".zip")) //解压zip
            {
                my_ns.MyUtils.uncompressDir(path, targetPathDir)
            }
            else//移动文件
            {
                var fileData: ArrayBuffer = native.fileUtils.getDataFromFile(path)
                var b = native.fileUtils.writeDataToFile(fileData, targetPath)
                if (!b) //移动失败
                {
                    this.finishCall(StateCode.MOVE_FILE_FAIL, file.fileName) //移动文件失败
                    break
                }
            }

        }


        onComplete()

    }
    /**
     * 热更新成功
     */
    protected updateSuccess() {
        console.log("updateSuccess", PackageOutPath)

        native.fileUtils.writeStringToFile(JSON.stringify(this.remoteMD5Config), PackageOutPath)
        //保存最新的配置到本地
        this.finishCall(StateCode.UPDATE_SUCCESS)
    }
    /**
     * 获取远程md5配置
     */
    protected getRemoteMD5Config(onFinish: Function) {

        var platform = this.getPlatForm()
        var baseUrl = `${this.remoteConfig.baseUrl}/${platform}/version_${this.remoteConfig.scriptVersion}/`
        var url = baseUrl + this.remoteConfig.configFile
        console.log("getRemoteMD5Config", url)
        this.sendHttpRequest(url, (err: boolean, response: string) => {
            if (err) {
                onFinish(0)
            }
            else {
                this.remoteMD5Config = JSON.parse(response)
                onFinish(1)
            }

        })
    }
    /**
     * 获取当前平台
     * @returns 
     */
    protected getPlatForm(): string {
        return sys.platform.toLowerCase()
    }

    /**
     * get请求
     * @param url 
     * @param responce 
     * @param responseType 
     */
    protected sendHttpRequest(url: string, responce: (err: boolean, data) => void, responseType: XMLHttpRequestResponseType = "") {
        var xhr = new XMLHttpRequest()
        xhr.responseType = responseType
        xhr.onreadystatechange = function () {

            if (xhr.readyState == 4 && (xhr.status >= 200 && xhr.status < 300)) {
                responce(false, xhr.response);
            }
        }
        xhr.onerror = function (err) {
            responce(true, null)
        };
        xhr.ontimeout = function () {
            responce(true, null)
        };
        xhr.onabort = function () {
            responce(true, null)
        }
        xhr.open("GET", url, true);

        xhr.timeout = 5000;

        xhr.send();
    }


    //===========================外部调用===========================
    /**
     * 检查更新
     * @param url 
     * @param finishCall 
     * @param progressCall 
     */
    checkUpdate(url: string, finishCall: FinishCall, progressCall: ProgressCall) {
        this.hotUrl = url
        this.finishCall = finishCall
        this.progressCall = progressCall

        this.startUpdate()
    }
    /**
     * 获取本地版本号
     */
    getLocalVersion(): number {
        return this.localConfig.scriptVersion
    }
    /**
     * 获取远程版本号
     * @returns 
     */
    getRemoteVersion(): number {
        return this.remoteConfig.scriptVersion
    }

    private static _instance: VersionManager;

    public static get Instance(): VersionManager {
        if (!this._instance) {
            if (NATIVE) {
                if (!native.fileUtils.isDirectoryExist(DownTempDir)) {
                    native.fileUtils.createDirectory(DownTempDir)
                }
                if (!native.fileUtils.isDirectoryExist(WriteableDir)) {
                    native.fileUtils.createDirectory(WriteableDir)
                }
                if (!native.fileUtils.isDirectoryExist(ConfigPath)) {
                    native.fileUtils.createDirectory(ConfigPath)
                }
            }
            this._instance = new VersionManager()
        }
        return this._instance;
    }




    //===========================子游戏===========================

    protected localSubGameInfoMap: Map<number, subGame> = new Map<number, subGame>()
    /**
     * 获取本地子游戏信息
     * @param subGameInfo 
     */
    protected parseLocalSubGameInfo(subGameInfo: subGameInfo) {
        this.localSubGameInfoMap.clear()
        var games = subGameInfo.subgames
        console.log("parseLocalSubGameInfo", games)
        for (var key in games) {
            var game: subGame = games[key]
            var id = game.ID

            var data: subGame = { ID: id, version: -1 }
            var path = `${WriteableDir}/${SubGameDirName}/${id}/${fileName}`

            if (native.fileUtils.isFileExist(path)) {
                var hotCfg: hotConfig = JSON.parse(native.fileUtils.getStringFromFile(path)) as hotConfig
                data.version = hotCfg.scriptVersion
                data.files = hotCfg
            }
            this.localSubGameInfoMap.set(id, data)
        }
    }

    /**
     * 获取远程子游戏版本号
     * @param gameID 
     * @returns 
     */
    getRemoteGameVersion(gameID: number): number {
        let subGames: Map<string, subGame> = this.getRemoteGameInfo().subgames
        let subGame = subGames[gameID.toString()]
        if (subGame) {
            return subGame.version
        }
        return 0
    }
    /**
     * 获取本地子游戏版本号
     * @param gameID 
     */
    getLocalGameVersion(gameID: number): number {
        return this.localSubGameInfoMap.get(gameID).version
    }

    /**
     * 远程子游戏信息
     */
    protected remoteSubGameInfo: subGameInfo = null
    /**
     * 获取远程子游戏信息
     * @returns 
     */
    protected getRemoteGameInfo(): subGameInfo {
        return this.remoteSubGameInfo
    }


    /**
     * 解析远程子游戏配置信息
     * @param url 
     * @param onFinish 
     */
    parseRemoteSubGameInfo(url: string, onFinish?: Function) {
        this.sendHttpRequest(url, (err: boolean, response: string) => {
            if (err) {
                console.log("parseRemoteSubGameInfo err", url)
                return
            }
            this.remoteSubGameInfo = JSON.parse(response) as subGameInfo
            this.parseLocalSubGameInfo(this.remoteSubGameInfo)

            onFinish?.()
        })
    }
    /**
     * 获取子游戏当前状态
     * @param gameID 
     * @returns 
     */
    getSubGameState(gameID: number): SubGameState {

        //本地版本
        var localVersion = this.getLocalGameVersion(gameID)
        //远程版本
        var remoteVersion = this.getRemoteGameVersion(gameID)
        var state = SubGameState.UN_KNOW
        if (localVersion == -1) //需要下载
        {
            state = SubGameState.NEED_DOWNLOAD
        }
        else if (localVersion == remoteVersion) //版本一致 不用更新
        {
            state = SubGameState.NO_NEED_UPDATE
        }
        else if (localVersion < remoteVersion) //本地版办 < 远程版本
        {
            state = SubGameState.NEED_UPDATE
        }


        return state
    }

    /**
     * 开始下载子游戏
     * @param gameID 
     * @param onComplete 
     * @param progressCall 
     */
    downLoadSubGame(gameID: number, onComplete: SubGameFinishCall, progressCall: ProgressCall) {
        //本地配置
        var localCfg: hotConfig = this.localSubGameInfoMap.get(gameID).files

        //获取远程子游戏配置
        var remoteSubGameInfo = this.getRemoteGameInfo()
        //当前平台
        var platform = this.getPlatForm()
        //根地址
        var baseUrl = `${remoteSubGameInfo.baseUrl}/${platform}/version_${this.remoteConfig.scriptVersion}/`
        //远程版本
        var remoteVersion = this.getRemoteGameVersion(gameID)

        var url = `${baseUrl}${SubGameDirName}/${gameID}/${fileName}.txt`
        console.log("downLoadSubGame", url)
        this.sendHttpRequest(url, (err: boolean, response: string) => {
            if (err) {
                console.log("获取远程子游戏MD5配置失败", url)  
                onComplete(SubGameState.GET_MD5_CONFIG_FAIL, fileName)
                return

            }
            //远程配置
            var remoteCfg = JSON.parse(response) as hotConfig

            //对比差异文件
            var localfiles = localCfg ? localCfg.files : new Map<string, fileInfo>()
            var changeList = this.compareChangeList(localfiles, remoteCfg.files)


            //下载差异文件
            this.downChangeFiles(baseUrl, changeList, (fileName: string) => {
                onComplete(SubGameState.DOWNLOAD_FAIL, fileName)
            },
                (downSize: number, totalSize: number) => {
                    progressCall(downSize, totalSize)
                },
                (moveList: fileInfo[]) => {
                    this.moveFiles(moveList, () => {
                        //写入本地配置
                        var filePath = `${WriteableDir}/${SubGameDirName}/${gameID}/${fileName}`
                        console.log("写入本地配置", filePath)
                        native.fileUtils.writeStringToFile(JSON.stringify(remoteCfg), filePath)
                        //更新本地子游戏版本号
                        var localData = this.localSubGameInfoMap.get(gameID)
                        localData.version = remoteVersion
                        this.localSubGameInfoMap.set(gameID, localData)
                        onComplete(SubGameState.UPDATE_SUCCESS)
                    })
                })
        })
    }

}